package org.forum.jstun;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

import org.forum.jstun.StunMessage.StunAddress;

public class PeriodDetector implements Runnable {
	private String mServerHost;
	private InetAddress mServerAddress;
	private int mServerPort;
	private DatagramSocket mSocket;
	private int mReceiveTimeout = 300;
	private int mReceiveTimespan = 1000*5;
	private InetAddress mMappedAddress;
	private int mMappedPort;
	private long mPeriodLower;
	private long mPeriodUpper;
	private boolean mActive;

	public interface BoundListener {
		public void onUpdate(long min, long max);
	}

	public interface PeriodListener {
		public void onFinish(long period);
	}

	private BoundListener mBoundListener;
	private PeriodListener mPeriodListener;

	public PeriodDetector(String server, int port) {
		mServerHost = server;
		mServerPort = port;
	}

	public void setBoundListener(BoundListener listener) {
		mBoundListener = listener;
	}

	public void setPeriodListener(PeriodListener listener) {
		mPeriodListener = listener;
	}

	@Override
	public void run() {
		detect();
		if (mPeriodListener!=null) mPeriodListener.onFinish(mPeriodLower);
	}

	public void stop() {
		mActive = false;
		if (mSocket!=null) mSocket.close();
		mSocket = null;
		if (mPeriodListener!=null) mPeriodListener.onFinish(mPeriodLower);
	}

	private void detect() {
		try {
			mServerAddress = InetAddress.getByName(mServerHost); 
			mSocket = new DatagramSocket();
			mSocket.setSoTimeout(mReceiveTimeout);
			System.out.println("connect to server "+mServerAddress.getHostAddress()+":"+mServerPort);
			mSocket.connect(mServerAddress, mServerPort);
		} catch (Exception e) {
			e.printStackTrace();
			stop();
			return;
		}
		mActive = true;
		if (detectBound())
			detectPeriod();
	}

	private StunMessage sendAndReceive(DatagramSocket socket, StunMessage msg) {
		byte[] data = msg.getBytes();
		DatagramPacket packet = new DatagramPacket(data, data.length);
		// already connect to destination (stun server)
		// stun over UDP without stream index, internet MTU recommend it is 576,
		// udp data length max is 548 (IP 20, UDP header 8).
		DatagramPacket receive = new DatagramPacket(new byte[1024], 1024);
		StunMessage response = null;
		for(int ts=0; response==null && ts<mReceiveTimespan; ts+=mReceiveTimeout) {
			try {
				socket.send(packet);
				while(true) {
					// already set socket SoTimeout
					socket.receive(receive);
					if (!receive.getAddress().equals(packet.getAddress()) ||
								receive.getPort()!=packet.getPort())
						continue;
					// parse receive message
					response = StunMessage.parseHeader(receive.getData());
					if (response==null) continue;
					if (StunMessage.transactionIdEquals(msg.getTransactionId(), 
							response.getTransactionId())) {
						response = StunMessage.parse(receive.getData(), receive.getLength());
						break;
					}
					response = null;
				}
			} catch (Exception e) {}
		}
		return response;
	}

	private boolean detectBound() {
		mPeriodLower = 0;
		mPeriodUpper = 1000;
		boolean ret = false;
		while(mActive) {
			try {
				StunMessage request = new StunMessage(StunMessage.TYPE_BINDING_REQUEST);
				request.setTransactionId(StunMessage.generateId(false));
				StunMessage response = sendAndReceive(mSocket, request);
				if (response==null) return false;
				StunAttribute sa = response.getAttribute(StunAttribute.TYPE_MAPPED_ADDRESS);
				if (sa==null) return false;
				InetAddress ma = ((StunAddress)sa).getAddress();
				int mp = ((StunAddress)sa).getPort();
				if (mMappedAddress==null) {
					System.out.println("detect mapped "+ma.getHostAddress()+":"+mp);
					mMappedAddress = ma;
					mMappedPort = mp;
				} else if (mMappedAddress.equals(ma) && mMappedPort==mp) {
					mPeriodLower = mPeriodUpper;
					mPeriodUpper *= 2;
					if (mBoundListener!=null) 
						mBoundListener.onUpdate(mPeriodLower, mPeriodUpper);
				} else {
					// mapped address changed, found bound
					System.out.println("mapped "+mMappedAddress.getHostAddress()+":"+mMappedPort
							+" changed to "+ma.getHostAddress()+":"+mp);
					mMappedAddress = ma;
					mMappedPort = mp;
					ret = true;
					break;
				}
				Thread.sleep(mPeriodUpper);
			} catch (Exception e) {
			}
		}
		return ret;
	}

	private boolean detectPeriod() {
		long period = (mPeriodLower+mPeriodUpper)/2;
		boolean ret = false;
		while (mActive && !ret) {
			try {
				Thread.sleep(period);
				StunMessage request = new StunMessage(StunMessage.TYPE_BINDING_REQUEST);
				request.setTransactionId(StunMessage.generateId(false));
				StunMessage response = sendAndReceive(mSocket, request);
				if (response==null) return false;
				StunAttribute sa = response.getAttribute(StunAttribute.TYPE_MAPPED_ADDRESS);
				if (sa==null) return false;
				InetAddress ma = ((StunAddress)sa).getAddress();
				int mp = ((StunAddress)sa).getPort();
				if (ma.equals(mMappedAddress) && mp==mMappedPort) {
					mPeriodLower = period;
				} else {
					// mapped address changed, found bound
					System.out.println("mapped "+mMappedAddress.getHostAddress()+":"+mMappedPort
							+" changed to "+ma.getHostAddress()+":"+mp);
					mMappedAddress = ma;
					mMappedPort = mp;
					mPeriodUpper = period;
				}
				if (mBoundListener!=null) 
					mBoundListener.onUpdate(mPeriodLower, mPeriodUpper);
			} catch (Exception e){}
			period = (mPeriodLower+mPeriodUpper)/2;
			ret = period/1000==mPeriodLower/1000;
		}
		return ret;
	}
	
}